﻿#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
//int main()
//{
//const 修饰指针
	//const int a = 10; //a具有了常属性(不能被修改了)
	//a = 20; //err
	//int arr[a]; //虽然a具有了常属性，但int arr[a]仍会报错
	//说明虽然a不能被修改了，但本质上仍是变量
	//但在C++ 中const修饰的变量就是常量

	//但是如果绕过n，使用n的地址，去修改n就能做到了，但是这样做是在打破语法规则。
	/*const int a = 10;
	int* pa = &a;
	*pa = 20;
	printf("a = %d\n", a);*/
	//但是这样做是不合理的，所以应该让p拿到n的地址也不能修改n

//const 修饰指针变量
	//一般来讲const修饰指针变量，可以放在* 的左边，也可以放在* 的右边，但两者意义是不一样的。
//const放在* 的右边：
	//int a = 10;
	//int b = 20;
	//int* const p = &a;
	////p = &b; //err
	//*p = 20; //可以执行   
	//printf("a = %d\n", a); //a = 20
	//此时const限制的是指针变量p本身，指针变量不能再指向其他变量了
	//但是*p并没有被固定，即仍可以通过指针变量，修改指针变量指向的内容

//const放在* 的左边：
	//int a = 10;
	//int b = 20;
	//int const* p = &a;
	//p = &b; //可以执行
	////*p = 10; //err
	//此时const限制的是指针变量指向的内容，即*p，不能通过指针修改指向的内容
	//但是指针变量p本身并没有被固定，即可以修改指针变量本身的值(即指针变量的指向)

    //如果const同时放在* 的左右两边，则上面两种限制都会执行，
	//即 -->  既不能修改指针变量指向的内容(*p = 10;//err)也不能修改指针变量p本身的值(p = &b;//err)

//指针运算
	//指针的基本运算有三种，分别是：
	//	指针 +/- 整数
	//	指针 - 指针
	//	指针的关系运算

//指针 +/- 整数
	//在前面我们已经知道：
	//int a = 10;
	//int* p = &a;
	//p + 1 --> 跳过4个字节
	//      --> 1 * sizeof(int)
	//推而广之：
	//type* p = &a;
	//p + 1 --> 跳过 1 * sizeof(type)
	//p + n --> 跳过 n * sizeof(type)

//用指针的方式也能实现数组元素的打印(指针 +/- 整数)
	//int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	//int sz = sizeof(arr) / sizeof(arr[0]);
	//int* p = &arr[0];
	//for (int i = 0; i < sz; i++)
	//{
	//	printf("%d ", *p); //第一种打印方法
	//	p++; //p = p + 1;(指针 +/- 整数)

	//	printf("%d ", *(p + i)); //第二种打印方法
	//}

//指针 - 指针
	//指针 - 指针(绝对值) --> 得到的是两个指针之间的元素个数
	//int arr[10] = { 0 };
	//printf("%zd\n", &arr[9] - &arr[0]); //9
	//printf("%zd\n", &arr[0] - &arr[9]); //-9
	//计算的前提条件：两个指针指向了同一块空间！
	//int arr[10] = { 0 };
	//int ch[5] = { 0 };
	//printf("%zd\n", &ch[4] - &arr[6]); //不会报错，但得出的数据没有意义
	//return 0;
//}

//求字符串的长度
#include <string.h>
//代码1：
//int  main()
//{
//	//strlen -- 求字符串长度的, strlen统计的是字符串\0之前的字符个数
//	char arr[] = "abcdef";
//	//[a b c d e f \0]
//	int len = strlen(arr);
//	printf("%d", len);
//	return 0;
//}

//代码2：
	//如果不用strlen函数
//int my_strlen(char* str)
//{
//	int count = 0;
//	while (*str != '\0')
//	{
//		count++;
//		str++; //用到了指针 +/- 整数的方法
//	}
//	return count;
//}
//int main()
//{
//	char arr[] = "abcdef";
//	int len = my_strlen(arr); //补充：之前我们知道在写scanf时如果是数组名就不用加取地址符&，原因就是数组名就是地址
//	printf("%d", len);        //其实数组名就是数组首元素的地址 --> arr == &arr[0]，可理解为arr == p(p = &arr[0])
//	return 0;
//}

//代码3：
//int my_strlen(char* str)
//{
//	int start = str; //先把数组首元素的地址放起来
//	while (*str != '\0')
//	{
//		str++;
//	}
//	//此时*str = '\0'
//	int len = str - start; //用到了指针 - 指针的方法
//	return len;
//}
//int main()
//{
//	char arr[] = "abcdef";
//	int len = my_strlen(arr); //补充：之前我们知道在写scanf时如果是数组名就不用加取地址符&，原因就是数组名就是地址
//	printf("%d", len);        //其实数组名就是数组首元素的地址 --> arr == &arr[0]，可理解为arr == p(p = &arr[0])
//	return 0;
//}

//指针的关系运算(指针和指针比较大小)
//int main()
//{
//	int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//	int sz = sizeof(arr) / sizeof(arr[0]);
//	int* p = arr; // == int* p = &arr[0]
//	while (p < arr + sz) //指针和指针比较大小(指针 +/- 整数)
//	{
//		printf("%d ", *p);
//		p++;
//	}
//	return 0;
//}

//野指针
    //概念：野指针就是指针指向的位置是不可知的（随机的、不正确的、没有明确限制的）

//野指针成因
//1. 指针未初始化
//int main()
//{
//	int* p; //局部变量指针未初始化，默认为随机值 
//	*p = 20;
//	return 0;
//}

//2. 指针越界访问
//int main()
//{
//	int arr[10] = { 0 };
//	int* p = &arr[0];
//	int sz = sizeof(arr) / sizeof(arr[0]);
//	int i = 0;
//	for (i = 0; i <= sz; i++)
//	{ 
//		printf("%d ", *p); //当指针指向的范围超出数组arr的范围时，p就是野指针
//		p++;
//	}
//	return 0;
//}

//3. 指针指向的空间释放
//int* test()
//{
//	int n = 10;
//	return &n;
//}
//int main()
//{
//	int* p = test();
//	printf("%d\n", *p);
//	return 0;
//}

//如何规避野指针
//1.指针初始化
    //如果明确知道指针指向哪⾥就直接赋值地址，如果不知道指针应该指向哪⾥，可以给指针赋值NULL
//int main()
//{
//	int a = 10;
//	int* p1 = &a;
//	int* p2 = NULL; //这条代码就是在提醒我们p2是一个空指针，我们要引起注意，我们不能使用这个p2
//	return 0;
//}
    // NULL 是C语言中定义的一个标识符常量，值是0，0也是地址，这个地址是无法使用的，读写该地址会报错。
    //原因是在我们电脑内存中并不是所有内存我们都能使用，而是有一个专门的空间留给“系统内核”的，我们不能使用

//2.小心指针越界
    //一个程序向内存申请了哪些空间，通过指针也就只能访问哪些空间，不能超出范围访问，超出了就是越界访问。

//3.指针变量不再使用时，及时置NULL，指针使用之前检查有效性
    //当指针变量指向一块区域的时候，我们可以通过指针访问该区域，
    //后期不再使用这个指针访问空间的时候，我们可以把该指针置为NULL。
    //因为约定俗成的一个规则就是：只要是NULL指针就不去访问，同时使用指针之前可以判断指针是否为NULL。

//assert 断言
    // assert.h 头文件定义了宏 assert() ，用于在运行时确保程序符合指定条件，如果不符合，就报错终止运行。
	// 这个宏常常被称为“断言”。
#include <assert.h>
//int main()
//{
//	int* p = NULL;
//	assert(p != NULL);
//	return 0;
//}
    //上面代码在程序运行到 assert(p != NULL); 语句时，验证变量 p 是否等于 NULL 。
	// 如果确实不等于 NULL ，程序继续运行，否则就会终止运⾏，并且给出报错信息提示。

//assert() 宏接受一个表达式作为参数。如果该表达式为真（返回值非零）， assert() 不会产生任何作用，程序继续运行。
    //如果该表达式为假（返回值为零）， assert() 就会报错，在标准错误流 stderr 中写入一条错误信息，
    //显示没有通过的表达式，以及包含这个表达式的文件名和行号。

//assert() 的使用对程序员是非常友好的，使⽤ assert() 有几个好处：
    //它不仅能自动标识文件和出问题的行号，还有⼀种无需更改代码就能开启或关闭 assert() 的机制。
    //如果已经确认程序没有问题，不需要再做断言，就在 #include <assert.h> 语句的前面，定义⼀个宏 NDEBUG 。
    //#define NDEBUG
    //#include <assert.h>
    //然后，重新编译程序，编译器就会禁用文件中所有的 assert() 语句。
	//如果程序又出现问题，可以移除这条 #define NDEBUG 指令（或者把它注释掉），再次编译，这样就重新启用了 assert() 语句。

//指针的使用和传址调用
//strlen的模拟实现
//size_t my_strlen(const char* str) //const作用是保险起见，如果有人在函数内部擅自改动*str的值而不是原来的"abcdef"
//{                                 //从而保证了 *str 固定，只要有人擅自改动 *str 的值则直接报错
//	size_t count = 0;
//	assert(str != NULL);//此代码目的是防止传过来的是空指针导致后续运行出问题，在此检验str是否为空指针
//	while (*str != '\0')
//	{
//		count++;
//		str++;
//	}
//	return count;
//}
//int main()
//{
//	char arr[] = "abcdef";
//	size_t len = my_strlen(arr);
//	printf("%zd\n", len);
//	return 0;
//}

//传值调用和传址调用
//写⼀个函数，交换两个整型变量的值
//void swap1(int x, int y)
//{
//	int tmp = 0;
//	tmp = x;
//	x = y;
//	y = tmp;
//}
//int main()
//{
//	int a = 10;
//	int b = 20;
//	printf("交换前：a = %d b = %d\n", a, b); //a = 10 b = 20
//	//之前我们了解到：当实参传递给形参的时候，形参相当于是实参的临时拷贝
//	//当我们对形参进行修改时，实参并不会被影响(本质是因为实参形参的地址是独立的)
//	swap1(a, b); //传值调用
//	printf("交换后：a = %d b = %d\n", a, b); //a = 10 b = 20
//	return 0;
//}

//void swap2(int* x, int* y)
//{
//	int tmp = 0;
//	tmp = *x; //tmp = a
//	*x = *y; //a = b
//	*y = tmp; //b = tmp
//}
//int main()
//{
//	int a = 10;
//	int b = 20;
//	printf("交换前：a = %d b = %d\n", a, b); //a = 10 b = 20
//	swap2(&a, &b); //传址调用
//	printf("交换后：a = %d b = %d\n", a, b); //a = 20 b = 10
//	return 0;
//}

//传址调用，可以让函数和主调函数之间建立真正的联系，在函数内部可以修改主调函数中的变量；
//所以未来函数中只是需要主调函数中的变量值来实现计算，就可以采用传值调用。
//如果函数内部要修改主调函数中的变量的值，就需要传址调用。